'''
    Converge-to-the-max Synchronous simulation
'''

import sys
from pathlib import Path
current_file_path = Path(__file__)
project_root = current_file_path.parent.parent.parent
sys.path.append(str(project_root))

import math
import simpy
import numpy as np
import pandas as pd
import random
import logging

from model import (
    Node,
    SimpleClock, 
    SimplePhy,
    Topology,
    TopologyChannel
)

logger = logging.getLogger('Test')



# -------- Simulation Parameters --------
CLOCK_FREQ_HZ = 1000000                         # 时钟频率(Hz)
CLOCK_SKEW_PPM_RANGE = (-500,500)              # 时钟漂移范围(ppm)
CLOCK_MAX = 500000                               # 时钟初始值随机生成最大值
CLOCK_INIT_MAX = 1000
SLOT_LENGTH = 5000                              # 时隙长度，时钟个数
FRAME_LENGTH = 10                               # 时帧长度，时隙个数
TX_FRONT_GAP = 200                              # 时隙前间隔，时钟个数
TX_BACK_GAP = 800                               # 时隙后间隔，时钟个数
CHANNEL_DELAY = 300 / CLOCK_FREQ_HZ             # 信道延迟，秒
CHANNEL_COLLISION_PROB = 0.9                    # 信道冲突率
NUM_NODES = 10                                   # 节点数
IF_COLLECT_NODE_EVENT = True                   # 是否收集节点事件
SAMPLE_FREQ_HZ = CLOCK_FREQ_HZ                  # 采样频率
SIM_DURATION = 1                              # 仿真时长
SIM_RECORD_FILE = 'ConvergeToTheMaxSim.csv'     # 保存文件名
# ---------------------------------------



class TestMac():

    def __init__(self, env, node, phy):
        self.env = env
        self.phy = phy
        phy.set_node(node)
        self.node = node

    def start(self):
        self.env.process(self.phy.start(self.node))
        self.env.process(self.timeslot_event())
        self.env.process(self.receive_event())

    def timeslot_event(self):
        while True:
            yield self.phy.wait_timeslot()
            f, s = self.phy.fs()
            if self.node.id == s:
                self.env.process(self.phy.tx(self.node, {'message':'helloworld'}))

    def receive_event(self):
        while True:
            yield self.phy.wait_receive()

def time_str(ts):
    unit_map = [
        (1, 's'),
        (0.001, 'ms'),
        (0.000001, 'us'),
        (0.000000001, 'ns')
    ]
    for threshold, unit in unit_map:
        if ts >= threshold:
            return f"{ts / threshold:.2f}{unit}"
    return f"0.00 ns"

def freq_str(f):
    unit_map = [
        (1e9, 'GHz'),
        (1e6, 'MHz'),
        (1e3, 'KHz'),
        (1, 'Hz')
    ]
    for threshold, unit in unit_map:
        if f >= threshold:
            return f"{f / threshold:.2f}{unit}"
    return f"0.00 Hz"

def sim_run():

    logger.info(f"Simulation converge-to-the-max synchronous")

    logger.info(f'Num nodes       : {NUM_NODES}')
    logger.info(f'Clock           : {freq_str(CLOCK_FREQ_HZ)} {time_str(1/CLOCK_FREQ_HZ)}')
    logger.info(f'Slot            : {time_str(SLOT_LENGTH/CLOCK_FREQ_HZ)} {time_str(TX_FRONT_GAP/CLOCK_FREQ_HZ)} + {time_str((SLOT_LENGTH - TX_FRONT_GAP - TX_BACK_GAP)/CLOCK_FREQ_HZ)} + {time_str(TX_BACK_GAP/CLOCK_FREQ_HZ)}')
    logger.info(f'Channel delay   : {time_str(CHANNEL_DELAY)}')
    logger.info(f'Simulation time : {SIM_DURATION}s')

    env = simpy.Environment()

    nodes = [Node(env, i) for i in range(NUM_NODES)]

    top = Topology(nodes)
    top.full_distribute()

    channel = TopologyChannel(env, top, CHANNEL_DELAY, CHANNEL_COLLISION_PROB)

    for i, n in enumerate(nodes):
        skew_ppm = random.randint(CLOCK_SKEW_PPM_RANGE[0], CLOCK_SKEW_PPM_RANGE[1])
        if CLOCK_INIT_MAX == 0:
            init_value = 0
        else:
            init_value = random.randint(0, CLOCK_INIT_MAX)
        logger.info(f'node {i} clock init:{init_value} skew:{skew_ppm}')
        clock = SimpleClock(env, CLOCK_FREQ_HZ, init_value, CLOCK_MAX, skew_ppm)
        phy = SimplePhy(env, clock, TX_FRONT_GAP, TX_BACK_GAP, SLOT_LENGTH, FRAME_LENGTH)
        phy.set_channel(channel)
        mac = TestMac(env, n, phy)
        n.set_phy(phy)
        n.set_mac(mac)
        mac.start()

    collect_data = {
        'timestamp': [],
        'clk_err_max': [],
    }

    if IF_COLLECT_NODE_EVENT:
        for n in nodes:
            slot_key = f'n{n.id}_s'
            tx_key = f'n{n.id}_t'
            rx_key = f'n{n.id}_r'
            collect_data[slot_key] = []
            collect_data[tx_key] = []
            collect_data[rx_key] = []

    def calculate_cyclic_range(counters, max_val):
        """
        计算循环计数器的环形极差（避免Max→0跳变影响）
        
        参数：
            counters: list，单次采样的所有计数器值（如[98,99,0,1]）
            max_val: int/float，计数器的最大值（如100）
        
        返回：
            float，环形极差（真实波动程度）
        """
        # 处理样本数≤1的情况：无波动
        if len(counters) <= 1:
            return 0.0
        
        # 去重（不影响结果，减少计算量）+ 排序
        sorted_counts = sorted(list(set(counters)))
        n = len(sorted_counts)
        
        # 环形总长度（整数计数器：max_val+1；浮点数计数器：max_val，因无离散步长）
        if isinstance(max_val, int) and all(isinstance(c, int) for c in counters):
            cycle_length = max_val + 1
        else:
            cycle_length = max_val
        
        # 计算所有相邻间隙（含闭环：最后一个→第一个+cycle_length）
        gaps = []
        for i in range(1, n):
            gaps.append(sorted_counts[i] - sorted_counts[i-1])
        # 闭环间隙：第一个元素+cycle_length - 最后一个元素
        gaps.append((sorted_counts[0] + cycle_length) - sorted_counts[-1])
        
        # 环形极差 = 环形总长度 - 最大间隙
        max_gap = max(gaps)
        cyclic_range = cycle_length - max_gap
        
        return round(cyclic_range, 4)  # 保留4位小数，可调整

    def monitor(env, nodes):

        while True:
            yield env.timeout(1/SAMPLE_FREQ_HZ)
            collect_data['timestamp'].append(env.now)

            clocks = [n.phy.clock.clock() for n in nodes]
            clk_error_max = calculate_cyclic_range(clocks, CLOCK_MAX)
            collect_data['clk_err_max'].append(clk_error_max)

            if IF_COLLECT_NODE_EVENT:

                for n in nodes:
                    slot_key = f'n{n.id}_s'
                    tx_key = f'n{n.id}_t'
                    rx_key = f'n{n.id}_r'

                    phy = n.phy

                    if phy.clock.clock() % SLOT_LENGTH == 0:
                        collect_data[slot_key].append(1)
                    else:
                        collect_data[slot_key].append(0)

                    if phy.is_tx_valid:
                        collect_data[tx_key].append(1)
                    else:
                        collect_data[tx_key].append(0)

                    if phy.is_rx_valid:
                        collect_data[rx_key].append(1)
                    else:
                        collect_data[rx_key].append(0)

    env.process(monitor(env, nodes))

    logger.info('Simulation running......')
    env.run(SIM_DURATION)
    logger.info(f"Simulation finished {env.now}")

    df = pd.DataFrame(collect_data)
    df.to_csv(SIM_RECORD_FILE, index=False)
    logger.info(f"Simulation collect data save to {SIM_RECORD_FILE}")

def set_log_level(name, level):
    logger = logging.getLogger(name)
    logger.setLevel(level)

def log_init():
    logging.basicConfig(
        level=logging.WARNING,
        format='%(name)12s - %(levelname)9s - %(message)s',
    )
    set_log_level('Test', logging.DEBUG)
    set_log_level('SimplePhy', logging.INFO)
    set_log_level('SimpleClock', logging.INFO)
    set_log_level('TopChannel', logging.WARNING)

if __name__ == '__main__':
    log_init()
    sim_run()